package com.puzzlebazar.client.squaregrid.control;

import com.google.gwt.event.dom.client.MouseEvent;
import com.google.gwt.event.shared.EventHandler;
import com.puzzlebazar.client.puzzle.model.History;
import com.puzzlebazar.client.puzzle.model.HistoryMove;
import com.puzzlebazar.client.puzzle.model.MoveTimer;
import com.puzzlebazar.client.squaregrid.model.StateModel;
import com.puzzlebazar.client.util.Releasable;
import com.puzzlebazar.client.util.Vec2i;

public abstract class CellStateControl implements ComponentCallback, Releasable {

	private final StateModel stateModel;
	private final History history;	
	private final MoveTimer moveTimer;

	private boolean mouseIsDown = false;
	private int currentState = 0;
	private int mouseDownParity = 0;
	
	private long canOverwriteWhenDrawing[];
	private boolean useCheckeredDrawing[];
	
	/**
	 * Creates an object that can control the state of the cells in a 
	 * square grid state-based model. Implements various feel using mouse-clicks,
	 * and mouse-drags.
	 * 
	 * @param stateModel The model to control
	 * @param history The history track to control (null if no history is desired)
	 * @param moveTimer The timer model to extract the time at which the move have been performed
	 */
	public CellStateControl(StateModel stateModel, History history, MoveTimer moveTimer) {
		this.stateModel = stateModel;
		this.history = history;
		this.moveTimer = moveTimer;
		
		int nbStates = stateModel.getNbStates();
		assert nbStates < 64 : "Too many states for state control";		
		canOverwriteWhenDrawing = new long[nbStates];
		useCheckeredDrawing = new boolean[nbStates];
		                                  
		java.util.Arrays.fill(canOverwriteWhenDrawing, (1<<nbStates)-1);
		java.util.Arrays.fill(useCheckeredDrawing, false);
	}

	/**
	 * Finds the state that should be generated when a click event is encountered.
	 * Derived classes should override this method to determine the state to generate
	 * base on the Event and possibly the content of the cell in the specified location.
	 * 
	 * @param loc Location where the mouse-click event occured
	 * @return The numerical value of the state to generate
	 * @throws Exception
	 */
	public abstract int stateToGenerate( Vec2i loc, MouseEvent<? extends EventHandler> event  ) throws Exception;
	
	@Override
	public boolean mouseDown( Vec2i loc,  MouseEvent<? extends EventHandler> event ) {
		try {
			mouseIsDown = true;
			mouseDownParity = parity( loc );
			currentState = stateToGenerate( loc, event );
			HistoryMove historyMove = history.addMoveToHistory( stateModel.setState( loc, currentState, moveTimer.peekTime() ), stateModel );
			if( historyMove != null ) {
				stateModel.notifyObservers( historyMove.getPuzzleMove() );
				history.notifyObservers( historyMove );
				moveTimer.step();
				moveTimer.notifyObservers();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return true;
	}	
	
	@Override
	public boolean mouseUp( Vec2i loc, MouseEvent<? extends EventHandler> event ) {
		if ( mouseIsDown ) {
			mouseIsDown = false;
			return true;
		}
		
		return false;
	}
	
	@Override
	public boolean canHandleMouseEnter(Vec2i loc, MouseEvent<? extends EventHandler> event) {
		return mouseIsDown;
	}


	@Override
	public boolean mouseEnter(Vec2i loc, MouseEvent<? extends EventHandler> event) {
		if( mouseIsDown ) {
			try {
				if( !canDrawState( currentState, loc ) )
					return false;
				if( !canStateOverwriteWhenDrawing( currentState, stateModel.getState(loc) ) )
					return false;
				HistoryMove historyMove = history.addMoveToHistory( stateModel.setState( loc, currentState, moveTimer.peekTime() ), stateModel );
				if( historyMove != null ) {
					stateModel.notifyObservers( historyMove.getPuzzleMove() );
					history.notifyObservers( historyMove );
					moveTimer.step();
					moveTimer.notifyObservers();
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return mouseIsDown;
	}

	@Override
	public boolean mouseLeave(Vec2i loc, MouseEvent<? extends EventHandler> event) {
		return false;
	}
		
	
	/**
	 * Checks if the passed state can be used to overwrite another state when
	 * drawing states (dragging the mouse)
	 * 
	 * @param stateToWrite State to write
	 * @param stateToOverwrite State to overwrite
	 * @return True if stateToWrite can overwrite stateToOverwrite
	 * @throws Exception
	 */
	protected boolean canStateOverwriteWhenDrawing
		( int stateToWrite, int stateToOverwrite ) throws Exception {
		
		checkStates( stateToWrite, stateToOverwrite );
		
		return ( canOverwriteWhenDrawing[stateToWrite] & (1 << stateToOverwrite) ) != 0 ? true : false;
	}	
	
	/**
	 * Checks if the passed state can be used at the specified location when
	 * drawing states (dragging the mouse)
	 * 
	 * @param state State to write
	 * @param loc Location at which to draw it
	 * @return True if state can be drawn at this location
	 * @throws Exception
	 */
	protected boolean canDrawState( int state, Vec2i loc ) throws Exception {
				
		checkState(state); 
		
		if( !useCheckeredDrawing[state] ) return true;
		
		return parity(loc) == mouseDownParity; 		
	}


	/**
	 * Returns the parity (0 or 1) of the passed location
	 * 
	 * @param loc Location for which to compute parity
	 * @return 0 for even locations (0,0)... , 1 for odds (1,0)...
	 */
	private int parity(Vec2i loc) {
		return (loc.x + loc.y) % 2;
	}
	
	/**
	 * Makes only one state overwritable when drawing a specific state
	 * 
	 * @param stateToDraw State that is being drawn
	 * @param overwritableState State that can be overwritten
	 * @throws Exception
	 */
	public void setOnlyOverwritableState
		( int stateToDraw, int overwritableState ) throws Exception {
		checkStates(stateToDraw, overwritableState);
		
		canOverwriteWhenDrawing[stateToDraw] = 1 << overwritableState;
	}

	/**
	 * Adds a state that can be overwritten when drawing a specific state
	 * 
	 * @param stateToDraw State that is being drawn
	 * @param overwritableState State that can be overwritten
	 * @throws Exception
	 */
	public void addOverwritableState
	( int stateToDraw, int overwritableState ) throws Exception {
		checkStates(stateToDraw, overwritableState);
	
		canOverwriteWhenDrawing[stateToDraw] = 
			canOverwriteWhenDrawing[stateToDraw] | (1 << overwritableState);
	}
	
	/**
	 * Removes a state that can no longer be overwritten when drawing a 
	 * specific state
	 * 
	 * @param stateToDraw State that is being drawn
	 * @param overwritableState State that can no longer be overwritten
	 * @throws Exception
	 */
	public void removeOverwritableState
	( int stateToDraw, int overwritableState ) throws Exception {
		checkStates(stateToDraw, overwritableState);
	
		canOverwriteWhenDrawing[stateToDraw] = 
			canOverwriteWhenDrawing[stateToDraw] & ~(1 << overwritableState);
	}
	
	/**
	 * Enable or disable "checkered drawing" for the specified state. Checkered
	 * drawing makes it only possible to draw the state on a "checkerboard" around
	 * the first mouse click.
	 * 
	 * @param state State for which to enable/disable checkered drawing
	 * @param checkeredDrawing True to enable, false otherwise
	 * @throws Exception
	 */
	public void setCheckeredDrawing
	( int state, boolean checkeredDrawing ) throws Exception {
		checkState(state);
	
		useCheckeredDrawing[state] = checkeredDrawing;
	}

	
	/**
	 * Throws an exception if either of the two passed states is invalid
	 * 
	 * @param state1
	 * @param state2
	 * @throws Exception
	 */
	private void checkStates(int state1, int state2) throws Exception {
		int nbStates = stateModel.getNbStates();		
		if( state1 >= nbStates || state2 >= nbStates ) 
			throw new Exception( "Invalid state" ); 
	}
	

	/**
	 *  Throws an exception if the passed state is invalid
	 * 
	 * @param state
	 * @throws Exception
	 */
	private void checkState(int state) throws Exception {
		if( state >= stateModel.getNbStates() ) 
			throw new Exception( "Invalid state" );
	}	

	/**
	 * Returns the state at a given location
	 * 
	 * @param loc Cell location
	 * @return State at that cell
	 * @throws Exception 
	 */
	protected int getState(Vec2i loc) throws Exception {
		return stateModel.getState( loc );
	}

	/**
	 * @return The number of states
	 */
	protected int getNbStates() {
		return stateModel.getNbStates();
	}		

	@Override
	public void release() {
	}
}